﻿using System;
using System.Threading;

namespace Quail
{
    /// <summary>
    /// Wait parameters used in the Wait method
    /// </summary>
    public class WaitParameters
    {
        private int _duration, _counter;

        /// <summary>
        /// Initializes a new instance of the <see cref="WaitParameters"/> class.
        /// </summary>
        /// <param name="waittype">the wait type</param>
        public WaitParameters(string waittype)
        {
            WaitType = waittype;
            Duration = -1;
            PollingInterval = 1000;
            ThrowOnStatusFailTimeout = true;
            FailStatus = LogStatus.FailTimeout;
        }

        /// <summary>
        /// Gets or sets the delegate to use that must return True to satisfy the wait
        /// </summary>
        public Func<bool> Delegate
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets a short string describing the wait delegate
        /// </summary>
        public string WaitType
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the message to log when the wait fails
        /// </summary>
        public string FailMessage
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the log status to log when the wait is not satisfied within the WaitDuration
        /// </summary>
        public LogStatus FailStatus
        {
            get;
            set;
        }

        /// <summary>
        /// Gets a value indicating whether this wait parameters object is using the Duration property
        /// </summary>
        public bool UsingDuration
        {
            get
            {
                return _counter == 0;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this wait parameters object is using the Counter property
        /// </summary>
        public bool UsingCounter
        {
            get
            {
                return !UsingDuration;
            }
        }

        /// <summary>
        /// Gets or sets the number of milliseconds to wait for the wait to succeed (default: Browser.Timeout)  (exclusive of Counter)
        /// </summary>
        public int Duration
        {
            get
            {
                return _duration;
            }

            set
            {
                if (value < -1)
                    throw new ArgumentOutOfRangeException("value", "Must be >= -1");

                _duration = value;

                if (value > 0 || value == -1)
                    _counter = 0;
            }
        }

        /// <summary>
        /// Gets or sets the number of times the Delegate should be checked (exclusive of Duration)
        /// </summary>
        public int Counter
        {
            get
            {
                return _counter;
            }

            set
            {
                if (value < 0)
                    throw new ArgumentOutOfRangeException("value", "Must be >= 0");

                _counter = value;

                if (value > 0)
                    _duration = 0;
            }
        }

        /// <summary>
        /// Gets or sets the duration (in milliseconds) between calls to the wait delegate (default: 1000ms)
        /// </summary>
        public int PollingInterval
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets a value indicating whether to Throw an exception on timeout when FailStatus == LogStatus.Fail (default: True)
        /// </summary>
        public bool ThrowOnStatusFailTimeout
        {
            get;
            set;
        }

        /// <summary>
        /// Waits for the Delegate() to return true.
        /// </summary>
        /// <param name="parentTask">The parent task</param>
        /// <returns>True if the wait was satisfied, False if the wait was not satisfied and ThrowOnStatusFailTimeout == false, otherwise throws a TimeoutException</returns>
        public bool Wait(Task parentTask)
        {
            var task = parentTask.BeginTask("WaitParameters.Wait");
            task.AddAttribute("waitType", WaitType);
            
            try
            {
                if (Delegate())
                {
                    task.Finish();
                    return true;
                }

                var waitSatisfied = false;
                if (UsingCounter)
                {
                    for (var counter = 0; counter < Counter; counter++)
                    {
                        Thread.Sleep(PollingInterval);
                        waitSatisfied = Delegate();
                        if (waitSatisfied)
                            break;
                    }
                }
                else
                {
                    if (Duration == -1)
                        Duration = task.TestSession.Browser.Timeout;

                    var watch = new TestStopwatch(Duration);
                    do
                    {
                        Thread.Sleep(PollingInterval);
                        waitSatisfied = Delegate();
                    } while (!waitSatisfied && !watch.Expired);
                }

                if (!waitSatisfied)
                {
                    var msg = FailMessage ?? "Timed out waiting on: " + WaitType;
                    if (FailStatus.IsFailingStatus() && ThrowOnStatusFailTimeout)
                        throw new WaitTimeoutException(FailStatus, msg);

                    task.Finish(FailStatus, msg);
                    return false;
                }

                task.Finish();
                return true;
            }
            catch (Exception e)
            {
                task.Finish(e);
                throw;
            }            
        }

        /// <summary>
        /// Retries an action that may cause a TimeoutException up to retryCounter times
        /// </summary>
        /// <param name="activeTask">The currently active task</param>
        /// <param name="taskName">Name of task to perform</param>
        /// <param name="action">The action to perform (given the parent task</param>
        /// <param name="retryCounter">Number of times to try the action</param>
        public static void RetryTimeout(Task activeTask, string taskName, Action<Task> action, int retryCounter = 3)
        {
            RetryTimeout(
                activeTask,
                taskName,
                t =>
                    {
                        action(t);
                        return 0;
                    },
                retryCounter);
        }

        /// <summary>
        /// Retries an action that may cause a TimeoutException up to retryCounter times
        /// </summary>
        /// <typeparam name="T">The return type</typeparam>
        /// <param name="activeTask">The currently active task</param>
        /// <param name="taskName">Name of task to perform</param>
        /// <param name="action">The action to perform (given the parent task</param>
        /// <param name="retryCounter">Number of times to try the action</param>
        /// <returns>The result of the action</returns>
        public static T RetryTimeout<T>(Task activeTask, string taskName, Func<Task, T> action, int retryCounter = 3)
        {
            var task = activeTask.BeginTask(taskName);
            try
            {
                var timedOutAtLeastOnce = false;
                T result;

                for (var retry = 1; ; retry++)
                {
                    try
                    {
                        result = action(task);
                        break;
                    }
                    catch (WaitTimeoutException)
                    {
                        timedOutAtLeastOnce = true;
                        if (retry >= retryCounter)
                            throw;
                    }
                }

                if (timedOutAtLeastOnce && task.OverallStatus.IsFailingStatus())
                    // ignore any failures and finish the task using a Pass status                    
                    task.Finish(LogStatus.Pass);
                else
                    task.Finish();

                return result;
            }
            catch (Exception ex)
            {
                task.Finish(ex);
                throw;

            }
        }

        public class WaitTimeoutException : LogStatusException
        {
            public WaitTimeoutException(string message) : this(LogStatus.FailTimeout, message)
            {
            }

            public WaitTimeoutException(LogStatus status, string message) : base(status, message)
            {
            }

            public WaitTimeoutException(LogStatus status, string message, Exception innerException) : base(status, message, innerException)
            {
            }
        }
        
    
    }
}
